﻿using System;
using System.Linq;
using System.Text;
using VulnDBExplorer.Services;
using System.IO;
using Luna;
using VulnDBExplorer.Services.Impl;
using Luna.Framework;
using System.Collections.Generic;
using KMB.Library.Collections;
using Iesi.Collections;

namespace VulnDB.Action.DataCollectors.Services.Impl {

	public class CodebaseFileEvent : EventArgs {
	
		public string Filename { get; set; }
	}

	public class CodebaseProfile : IContextAwareObject {
	
		private List<ICodebase> codebases;
		private SortedList<string, ProfileEntry> files;

		public CodebaseProfile(IAppContext context) {
			codebases = new List<ICodebase>();
			files = new SortedList<string, ProfileEntry>();
			this.Context = context;
			MaxFile = int.MaxValue;
		}

		public IAppContext Context { get; set; }

		public IList<ICodebase> Codebases {
			get { return codebases; }
		}

		public IDictionary<string, ProfileEntry> Files {
			get { return files; }
		}

		public int MaxFile { get; set; }

		public void addCodebase(ICodebase codebase) {
			if (codebases.FindIndex(p => p.Name.Equals(codebase.Name)) < 0)
				codebases.Add(codebase);

			FileComparator comparator = new FileComparator();

			CodebaseFileEvent eventArg = new CodebaseFileEvent();

			foreach (string filename in codebase.enumerateSourceFiles())
				if (isSourceFile(filename)) {
					string relativeFile = codebase.getRelativePath(filename);
					ProfileEntry entry = null;
					
					eventArg.Filename = relativeFile;

					if (FileEntryProcessStarted != null)
						FileEntryProcessStarted(this, eventArg);

					if (!files.TryGetValue(relativeFile, out entry))
						files[relativeFile] = entry = new ProfileEntry() { Filename = relativeFile };

					if (!entry.FileSizes.ContainsKey(codebase.Name))
						entry.FileSizes[codebase.Name] = countLOC(filename);

					foreach (ICodebase cb in codebases)
						if (!cb.Name.Equals(codebase.Name)) {	
							string leftFile = cb.getFullPath(relativeFile);
							if (leftFile.fileExists()) {
								if (!entry.DiffChunks.ContainsKey("{0}-{1}@left".format(cb.Name, codebase.Name)) &&
									!entry.DiffChunks.ContainsKey("{1}-{0}@left".format(cb.Name, codebase.Name))) {
									FileCompareResult res = comparator.compare(leftFile, filename);
									entry.DiffChunks["{0}-{1}@left".format(cb.Name, codebase.Name)] = res.LeftChange;
									entry.DiffChunks["{0}-{1}@right".format(cb.Name, codebase.Name)] = res.RightChange;
								}
							}
							if (FileEntryCompareCompleted != null)
								FileEntryCompareCompleted(this, eventArg);
						}

					if (FileEntryProcessCompleted != null)
						FileEntryProcessCompleted(this, eventArg);

					MaxFile--;
					if (MaxFile <= 0) break;
				}
			
		}

		private int countLOC(string filename) {
			int loc = 0;
			using (var r = new StreamReader(filename))
				while (!r.EndOfStream) {
					string line = r.ReadLine();
					loc++;
				}
			return loc;
		}

		public static bool isSourceFile(string filename) {
			return true;
		}

		ICodebase resolveCodebase(string name) {
			ICodebase result = codebases.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
			if (result == null && Context != null) {
				foreach (IRepository repo in Context.findServices<IRepository>()) {
					result = repo.Codebases.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
					if (result != null) break;
				}
			}
			return result;
		}

		public void save(StreamWriter output) {
			output.WriteLine("Codebases:");
			foreach (ICodebase codebase in codebases) {
				output.WriteLine("@{0}".format(codebase.Name));
			}
			output.WriteLine("@@");
			output.WriteLine("Files: {0}".format(files.Count));
			foreach (ProfileEntry entry in files.Values)
				entry.save(output);
		}

		public void load(StreamReader input) {
			codebases.Clear();
			files.Clear();

			string line = input.ReadLine();
			if (!line.Equals("Codebases:"))
				throw new ApplicationException("Invalid file format: 'Codebases' section is required.");
			// load codebases section
			while (line != "@@" && !input.EndOfStream) {
				line = input.ReadLine();
				if (line != "@@" && line.StartsWith("@")) {
					ICodebase codebase = resolveCodebase(line.Substring(1));
					if (codebase == null)
						throw new ApplicationException("Codebase not found: " + line.Substring(1));
					codebases.Add(codebase);
				}
			}
			// load file section
			while (!line.StartsWith("Files:") && !input.EndOfStream)
				line = input.ReadLine();
			
			if (line.StartsWith("Files:")) {
				while (!input.EndOfStream) {
					ProfileEntry entry = ProfileEntry.load(input);
					if (entry != null)
						files.Add(entry.Filename, entry);
				}
			}
		}

		public void save(string filename) {
			using (var f = new StreamWriter(filename))
				save(f);
		}

		public static CodebaseProfile load(string filename, IAppContext context) {
			CodebaseProfile profile = new CodebaseProfile(context);
			using (var f = new StreamReader(filename))
				profile.load(f);
			return profile;
		}

		public event EventHandler<CodebaseFileEvent> FileEntryProcessStarted;

		public event EventHandler<CodebaseFileEvent> FileEntryCompareCompleted;

		public event EventHandler<CodebaseFileEvent> FileEntryProcessCompleted;
	}

	public class ProfileEntry {
		public string Filename { get; set; }
		public IDictionary<string, IList<FileCompareResult.FileChunk>> DiffChunks;
		public IDictionary<string, int> FileSizes;

		public ProfileEntry() {
			DiffChunks = new SortedList<string, IList<FileCompareResult.FileChunk>>();
			FileSizes = new SortedList<string, int>();
		}

		public void save(StreamWriter output) {
			output.WriteLine("@{0}".format(Filename));
			output.Write("\tFileSize:");
			foreach (string key in FileSizes.Keys)
				output.Write(" {0}={1}", key, FileSizes[key]);
			output.WriteLine();
			foreach (string key in DiffChunks.Keys) {
				IList<FileCompareResult.FileChunk> chunks = DiffChunks[key];
				output.Write("\t{0}:".format(key));
				foreach (FileCompareResult.FileChunk chunk in chunks) {
					output.Write(" {0}-{1}".format(chunk.Begin, chunk.End));
				}
				output.WriteLine();
			}
			output.WriteLine("@@");
		}

		public static ProfileEntry load(StreamReader input) {
			ProfileEntry result = null;
			string line = input.ReadLine();
			if (line != "@@" && line.StartsWith("@")) {
				result = new ProfileEntry();
				result.Filename = line.Substring(1);
				// load file sizes for each codebase
				while (!input.EndOfStream && !line.StartsWith("\tFileSize:"))
					line = input.ReadLine();
				if (!input.EndOfStream) {
					line = line.Substring("\tFileSize: ".Length);
					foreach(string pair in line.Split(' '))
						if (pair.isNotNullAndEmpty() && pair.IndexOf('=') > 0) {
							int idx = pair.IndexOf('=');
							string key = pair.Substring(0, idx);
							int value = pair.Substring(idx + 1).toInt(-1);
							if (value < 0)
								throw new ApplicationException("Invalid data format for file '{0}', section FileSize, key={1}".format(result.Filename, key));
							result.FileSizes[key] = value;
						}
				}
				while (line != "@@" && !input.EndOfStream) {
					line = input.ReadLine();
					if (line.StartsWith("\t") && line.IndexOf(':') > 0) {
						int idx = line.IndexOf(':');
						string key = line.Substring(1, idx - 1);
						List<FileCompareResult.FileChunk> chunks  = new List<FileCompareResult.FileChunk>();
						result.DiffChunks[key] = chunks;

						if (idx + 2 < line.Length) {
							foreach (string pair in line.Substring(idx + 2).Split(' '))
								if (pair.isNotNullAndEmpty() && pair.IndexOf('-') > 0) {
									idx = pair.IndexOf('-');
									int begin = pair.Substring(0, idx).toInt();
									int end = pair.Substring(idx + 1).toInt();
									chunks.Add(new FileCompareResult.FileChunk() { Begin = begin, End = end });
								}
						}
					}
				}
			}
			return result;
		}
	}

	public static class ProfileEntryExtension {

		public static ISet toIntSet(IList<FileCompareResult.FileChunk> chunks) {

			SetOfInts s = new SetOfInts();
			foreach(FileCompareResult.FileChunk chunk in chunks) 
				for (int i = chunk.Begin; i <= chunk.End; i++)
					s.Add(i);
			return s;
		}

		public static ISet toIntSet(FileCompareResult.FileChunk chunk) {
			return toIntSet(chunk.Begin, chunk.End);
		}

		public static ISet toIntSet(int min, int max) {
			return new SetBetween<int>(min, max);
		}

		public static ISet getUniqueLeftSet(this ProfileEntry entry, ICodebase left, ICodebase right) {
			if (left == null)
				return EmptySet.e;
			if (right == null || !entry.DiffChunks.ContainsKey("{0}-{1}@left".format(left.Name, right.Name)))
				return entry.FileSizes.ContainsKey(left.Name) ? toIntSet(1, entry.FileSizes[left.Name]) : EmptySet.e;
			else
				return toIntSet(entry.DiffChunks["{0}-{1}@left".format(left.Name, right.Name)]);
		}

		public static ISet getUniqueRightSet(this ProfileEntry entry, ICodebase left, ICodebase right) {
			if (right == null)
				return EmptySet.e;
			if (left == null || !entry.DiffChunks.ContainsKey("{0}-{1}@right".format(left.Name, right.Name)))
				return entry.FileSizes.ContainsKey(right.Name) ? toIntSet(1, entry.FileSizes[right.Name]) : EmptySet.e;
			else
				return toIntSet(entry.DiffChunks["{0}-{1}@right".format(left.Name, right.Name)]);
		}

		public static ISet getCommonSet(this ProfileEntry entry, ICodebase left, ICodebase right) {
			if (left == null || right == null || !entry.FileSizes.ContainsKey(left.Name) || !entry.FileSizes.ContainsKey(right.Name))
				return EmptySet.e;
			ISet commonSet = toIntSet(1, entry.FileSizes[left.Name]).Minus(getUniqueLeftSet(entry, left, right));
			return commonSet;
		}
	}

	public static class SetExtension {
		
		public static ISet union(this ISet a, ISet b) {
			if (a == null && b == null)
				return EmptySet.e;
			if (a == null) return b;
			if (b == null) return a;
			return new UnionSet(a, b);
		}

		public static ISet intersect(this ISet a, ISet b) {
			if (a == null || b == null || a.IsEmpty || b.IsEmpty)
				return EmptySet.e;
			return new IntersectSet(a, b);
		}

		public static ISet minus(this ISet a, ISet b) {
			if (a == null)
				return EmptySet.e;
			if (b == null)
				return a;
			return new MinusSet(a, b);
		}
	}
}
